#pragma once

#include "BaseWindow.h"
#include "dxres.h"
#include "IRender.h"
#include <vector>
#include <memory>
#include "common.h"

#include "BackgroundRender.h"
#include "MouseBallRender.h"
#include "StarSystemRender.h"
#include "FpsRender.h"

extern "C" IMAGE_DOS_HEADER __ImageBase;

static float const WindowWidth = 600.0f;
static float const WindowHeight = 600.0f;

using Microsoft::WRL::ComPtr;
using namespace D2D1;
using namespace std;


struct MainWindow : BaseWindow<MainWindow>, IRender
{
	dxres m_res;
	vector<unique_ptr<IRender>> m_renders;

	MainWindow() :
		IRender(m_res)
	{
		Create();
	}

	void AddRenders()
	{
		m_renders.push_back(
			make_unique<BackgroundRender>(m_res));
		m_renders.push_back(
			make_unique<StarSystemRender>(m_res));
		/*m_renders.push_back(
			make_unique<MouseBallRender>(m_res));*/
		m_renders.push_back(
			make_unique<FpsRender>(m_res));

		CreateDeviceIndependentResources();
	}

	void Create()
	{
		WNDCLASSEX wc = { sizeof(wc) };
		wc.lpfnWndProc = WndProc;
		wc.lpszClassName = L"sdsky";
		wc.hCursor = LoadCursor(nullptr, IDC_ARROW);
		wc.hInstance = reinterpret_cast<HINSTANCE>(&__ImageBase);

		RegisterClassEx(&wc);

		ASSERT(!m_window);

		CreateWindowEx(0, 
					   wc.lpszClassName, 
					   L"sdsky",
					   WS_OVERLAPPEDWINDOW | WS_VISIBLE,
					   CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
					   nullptr,
					   nullptr,
					   wc.hInstance,
					   this);

		ASSERT(m_window);
		m_res.window = m_window;

		AddRenders();
	}

	// IRender

	void Render() override
	{
		ValidateRect(m_window, nullptr);

		if (!IsDeviceCreated())
		{
			CreateDeviceResources();

			D2D1_SIZE_F size = m_res.target->GetSize();

			CreateDeviceSizeResources(size.width, size.height);
		}

		m_res.target->BeginDraw();

		for (auto & i : m_renders)
		{
			i->Render();
		}

		m_res.target->EndDraw();
		HRESULT hr = m_res.swapChain->Present(1, 0);

		if (hr != S_OK && hr != DXGI_STATUS_OCCLUDED)
		{
			ReleaseDeviceResources();
		}
	}

	void CreateDeviceIndependentResources() override
	{
		D2D1_FACTORY_OPTIONS fo = {};

		#ifdef _DEBUG
			fo.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
		#endif

		HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, 
							 fo, 
							 m_res.d2d.GetAddressOf()));

		HR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, 
							   __uuidof(m_res.dwrite), 
							   reinterpret_cast<IUnknown **>(m_res.dwrite.GetAddressOf())));

		HR(CoCreateInstance(CLSID_WICImagingFactory, 
							nullptr, 
							CLSCTX_INPROC_SERVER, 
							__uuidof(m_res.wic), 
							reinterpret_cast<void **>(m_res.wic.GetAddressOf())));

		for (auto & i : m_renders)
		{
			i->CreateDeviceIndependentResources();
		}
	}

	ComPtr<ID3D11Device> CreateD3Device()
	{
		UINT flag = D3D11_CREATE_DEVICE_BGRA_SUPPORT;

		#ifdef _DEBUG
			flag |= D3D11_CREATE_DEVICE_DEBUG;
		#endif

		ComPtr<ID3D11Device> device;
		HRESULT hr;

		hr = D3D11CreateDevice(nullptr, 
							   D3D_DRIVER_TYPE_HARDWARE, 
							   nullptr, 
							   flag, 
							   nullptr, 
							   0, 
							   D3D11_SDK_VERSION, 
							   device.GetAddressOf(), 
							   nullptr, 
							   nullptr);

		if (hr == S_OK) return device;

		hr = D3D11CreateDevice(nullptr, 
							   D3D_DRIVER_TYPE_WARP, 
							   nullptr, 
							   flag, 
							   nullptr, 
							   0, 
							   D3D11_SDK_VERSION, 
							   device.GetAddressOf(), 
							   nullptr, 
							   nullptr);

		HR(hr);
		return device;
	}

	void CreateDeviceResources() override
	{
		ComPtr<ID3D11Device> device = CreateD3Device();

		ComPtr<IDXGIDevice1> dxdevice;
		HR(device.As(&dxdevice));

		ComPtr<IDXGIAdapter> adapter;
		HR(dxdevice->GetAdapter(adapter.GetAddressOf()));

		ComPtr<IDXGIFactory2> dxfactory;
		HR(adapter->GetParent(__uuidof(dxfactory), 
							  reinterpret_cast<void **>(dxfactory.GetAddressOf())));

		ComPtr<ID2D1Device> d2device;
		HR(m_res.d2d->CreateDevice(dxdevice.Get(), 
								   d2device.GetAddressOf()));

		HR(d2device->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, 
										 m_res.target.GetAddressOf()));

		DXGI_SWAP_CHAIN_DESC1 desc = {};
		desc.BufferCount = 1;
		desc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_BACK_BUFFER;
		desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
		desc.SwapEffect = DXGI_SWAP_EFFECT_SEQUENTIAL;
		desc.SampleDesc.Count = 1;

		HR(dxfactory->CreateSwapChainForHwnd(device.Get(), 
											 m_window, 
											 &desc, 
											 nullptr, 
											 nullptr, 
											 m_res.swapChain.GetAddressOf()));

		ComPtr<IDXGISurface> surface;
		HR(m_res.swapChain->GetBuffer(0, __uuidof(surface), reinterpret_cast<void **>(surface.GetAddressOf())));

		ComPtr<ID2D1Bitmap1> bitmap;
		HR(m_res.target->CreateBitmapFromDxgiSurface(surface.Get(), 
													 BitmapProperties1(D2D1_BITMAP_OPTIONS_CANNOT_DRAW|D2D1_BITMAP_OPTIONS_TARGET, 
																	   PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM , 
																				   D2D1_ALPHA_MODE_PREMULTIPLIED), 
																	   m_res.dpi, m_res.dpi), 
													 bitmap.GetAddressOf()));
		m_res.target->SetTarget(bitmap.Get());

		for (auto & i : m_renders)
		{
			i->CreateDeviceResources();
		}
	}

	void CreateDeviceSizeResources(float width, float height) override
	{
		for (auto & i : m_renders)
		{
			i->CreateDeviceSizeResources(width, height);
		}
	}

	void ReleaseDeviceSizeResources() override
	{
		for (auto & i : m_renders)
		{
			i->ReleaseDeviceSizeResources();
		}
	}

	void ReleaseDeviceResources() override
	{
		m_res.target.Reset();

		for (auto & i : m_renders)
		{
			i->ReleaseDeviceResources();
		}

		ReleaseDeviceSizeResources();
	}

	void ReleaseDeviceIndependentResources() override
	{
		for (auto & i : m_renders)
		{
			i->ReleaseDeviceIndependentResources();
		}
	}

	LRESULT MessageHandler(UINT const message, 
						   WPARAM const wparam, 
						   LPARAM const lparam) override
	{
		for (auto & i : m_renders)
		{
			LRESULT result = i->MessageHandler(message, wparam, lparam);
			if (result != 0)
			{
				return result;
			}
		}

		if (message == WM_CREATE)
		{
			CreateHandler();
		}
		else if (message == WM_DPICHANGED)
		{
			DpiChangedHandler(wparam, lparam);
		}
		else if (message == WM_PAINT)
		{
			PaintHandler();
		}
		else if (message == WM_SIZE)
		{
			SizeHandler(wparam, lparam);
		}
		else if (message == WM_DESTROY)
		{
			DestroyHandler();
		}
		return BaseWindow::MessageHandler(message, wparam, lparam);
	}

	void DestroyHandler()
	{
		ReleaseDeviceResources();
		ReleaseDeviceIndependentResources();

		m_renders.clear();
	}

	void SizeHandler(WPARAM const wparam, LPARAM const lparam)
	{
		if (IsDeviceCreated() && wparam != SIZE_MINIMIZED)
		{
			D2D1_SIZE_U size = SizeU(LOWORD(lparam), 
									 HIWORD(lparam));

			
			m_res.target->SetTarget(nullptr);
			ReleaseDeviceSizeResources();
			HRESULT hr = m_res.swapChain->ResizeBuffers(0, 0, 0, DXGI_FORMAT_B8G8R8A8_UNORM, 0);

			if (hr != S_OK)
			{
				ReleaseDeviceResources();
			}
			else
			{
				ComPtr<IDXGISurface> surface;
				HR(m_res.swapChain->GetBuffer(0, __uuidof(surface), reinterpret_cast<void **>(surface.GetAddressOf())));

				ComPtr<ID2D1Bitmap1> bitmap;
				HR(m_res.target->CreateBitmapFromDxgiSurface(surface.Get(),
															 BitmapProperties1(D2D1_BITMAP_OPTIONS_CANNOT_DRAW|D2D1_BITMAP_OPTIONS_TARGET, 
																			   PixelFormat(DXGI_FORMAT_B8G8R8A8_UNORM, 
																						   D2D1_ALPHA_MODE_PREMULTIPLIED), 
																			   m_res.dpi, m_res.dpi), 
															 bitmap.GetAddressOf()));
				m_res.target->SetTarget(bitmap.Get());

				float logicalWidth = PhysicalToLogical(size.width, m_res.dpi);
				float logicalHeight = PhysicalToLogical(size.height, m_res.dpi);

				CreateDeviceSizeResources(logicalWidth, logicalHeight);
			}
		}

		VERIFY(InvalidateRect(m_window, nullptr, false));
	}

	void PaintHandler()
	{
		Render();
	}

	void CreateHandler()
	{
		HMONITOR const monitor = MonitorFromWindow(m_window, MONITOR_DEFAULTTONEAREST);
		
		unsigned dpiX = 96;
		unsigned dpiY = 96;

		/*HR(GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &dpiX, &dpiY));*/

		m_res.dpi = static_cast<float>(dpiX);

		AdjustWindowSize();
	}

	void DpiChangedHandler(WPARAM const wparam, LPARAM const lparam)
	{
		m_res.dpi = LOWORD(wparam);

		AdjustWindowSize();
	}

	void AdjustWindowSize()
	{
		RECT rect =
		{
			0,
			0,
			static_cast<unsigned>(LogicalToPhysical(WindowWidth, m_res.dpi)),
			static_cast<unsigned>(LogicalToPhysical(WindowHeight, m_res.dpi))
		};

		VERIFY(AdjustWindowRect(&rect,
			GetWindowLong(m_window, GWL_STYLE),
			false));

		VERIFY(SetWindowPos(m_window,
			nullptr,
			rect.left,
			rect.top,
			rect.right - rect.left,
			rect.bottom - rect.top,
			SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOZORDER));
	}
};